package com.leunpha.mini.downloader.nio;

import com.leunpha.mini.downloader.bean.FileBean;
import com.leunpha.mini.net.events.NetEvent;
import com.leunpha.mini.net.events.NetState;
import com.leunpha.mini.net.listener.Listener;
import com.leunpha.mini.net.listener.ServerListener;
import com.leunpha.mini.utils.StringUtils;

import java.io.File;
import java.io.IOException;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;

/**
 * User: zhoujingjie
 * Date: 14-6-27
 * Time: 下午2:51
 */
public class SocketDownloader extends ServerListener implements Runnable {
    private FileBean fileBean;                      //文件对象
    private static final int DEFAULT_PORT = 80;     //默认端口
    private Selector selector;                      //选择器
    private FileChannel outChannel;                 //客户端保存文件通道
    private int timeout = 10 * 1000;                //连接超时10秒
    private long start;                             //开始位置
    private SocketChannel socketChannel;
    private boolean rangeSupport;
    private ByteBuffer buffer = ByteBuffer.allocate(2048);
    private Map<String, String> responseHeaders = new HashMap<String, String>();
    private File downloadFile;

    public SocketDownloader(FileBean fileBean) {
        this.fileBean = fileBean;
    }

    public void run() {
        try {
            _run();
        } catch (final IOException e) {
            notifyListener(NetEvent.instance(NetState.ERROR, new HashMap() {
                {
                    put("throwable", e);
                    put("bean", fileBean);
                }
            }));
            e.printStackTrace();
        }
    }

    /**
     * 运行
     *
     * @throws IOException
     */
    private void _run() throws IOException {
        URL url = new URL(fileBean.getUrl());
        notifyListener(NetEvent.instance(NetState.BEGIN,fileBean));
        prepare(url);
        START:
        while (true) {
            int useTime = selector.select(timeout);
            if (useTime == 0) {
                throw new SocketTimeoutException(fileBean + " 连接超时");
            }
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();
                if (key.isConnectable()) {
                    connectable(url, key);
                } else if (key.isReadable()) {
                    if (readable(key) == -1) {
                        break START;
                    }
                }
            }
        }
        notifyListener(NetEvent.instance(NetState.END,fileBean));
    }

    private byte[] getRequestHeaders(URL url) {
        String urlFile = url.getFile();
        if ("".equals(urlFile)) {
            urlFile = "/";
        }
        StringBuilder headers = new StringBuilder();
        headers.append("GET ");
        headers.append(urlFile);
        headers.append(" HTTP/1.1\r\n");
        headers.append("Host: ");
        headers.append(url.getHost());
        headers.append("\r\n");
        headers.append("User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36\r\n");
        headers.append("Connection: keep-alive\r\n");
        headers.append("Accept: */*\r\n");
        headers.append("\r\n");
        return headers.toString().getBytes();
    }

    /**
     * 连接成功
     * =======
     *
     * @param url
     * @param key
     * @throws IOException
     */
    private void connectable(URL url, SelectionKey key, long start, long end) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        if (channel.isConnectionPending()) {
            channel.finishConnect();
        }
        channel.write(ByteBuffer.wrap(getRequestHeaders(url)));
        parseResponseHeader(channel);

        if("302".equals(responseHeaders.get("statusCode"))){
            String location = responseHeaders.get("Location");
            if(location != null && !"".equals(location.trim())){
                fileBean.setUrl(location);
                selector.close();
                _run();
            }
        }else{
            channel.register(selector, SelectionKey.OP_READ);
            outChannel = getOutChannel();
        }
    }

    private void connectable(URL url, SelectionKey key) throws IOException {
        connectable(url, key, start, -1);
    }

    /**
     * 可读
     *
     * @param key
     * @return
     * @throws IOException
     */
    private int readable(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        int len;
        if (buffer.position() != 0) {
            buffer.limit(buffer.position());
            buffer.position(0);
            outChannel.write(buffer);
            buffer.clear();
        }
        if ((len = channel.read(buffer)) > 0) {
            buffer.flip();
            fileBean.setReadLength(len);
            notifyListener(NetEvent.instance(NetState.RUNNING, fileBean));
            outChannel.write(buffer);
            buffer.clear();
            return 0;
        } else {
            socketChannel.close();
            outChannel.close();
            channel.close();
            notifyListener(NetEvent.instance(NetState.SUCCESS, fileBean));
            return -1;
        }
    }


    private void parseResponseHeader(SocketChannel channel) throws IOException {
        String headerString = getHeaderString(channel);
        String[] temp;
        for (String header : headerString.split("\r\n")) {
            temp = header.split(": ");
            if (temp.length == 2) {
                responseHeaders.put(temp[0], temp[1]);
            } else {
                String[] protocol = temp[0].split(" ");
                if (protocol.length >= 3) {
                    responseHeaders.put("status", protocol[2]);
                    responseHeaders.put("statusCode", protocol[1]);
                    try {
                        int code = Integer.parseInt(protocol[1]);
                        if (code / 100 != 2 && code != 302) {
                                throw new IllegalStateException("code:" + code);
                        }
                    } finally {
                    }
                    //rangeSupport = "206".equals(protocol[1]);
                }
            }
        }

        try {
            fileBean.setTotalLength(Long.parseLong(responseHeaders.get("Content-Length")));
        } catch (Exception e) {
            fileBean.setTotalLength(-1);
        }
    }

    private String getHeaderString(SocketChannel channel) throws IOException {
        ByteBuffer headerBuffer = ByteBuffer.allocate(1024);
        ByteArrayOutputStream headerOs = new ByteArrayOutputStream();
        boolean stop = false;
        long start = System.currentTimeMillis();
        while (!stop) {
            if (channel.read(new ByteBuffer[]{headerBuffer}) < 1) {
                if (System.currentTimeMillis() - start > timeout) {
                    throw new SocketTimeoutException("读取头信息超时;read header time out !");
                }
                continue;
            }
            byte[] headers = {'\r', '\n', '\r', '\n'};
            byte[] reades = headerBuffer.array();
            for (int i = 0; i < reades.length; i++) {
                if (i < reades.length - 3) {
                    if (reades[i] == headers[0] && reades[i + 1] == headers[1] && reades[i + 2] == headers[2] && reades[i + 3] == headers[3]) {
                        buffer.put(reades, i + 4, reades.length - i - 4);
                        stop = true;
                        break;
                    }
                }
                headerOs.write(reades[i]);
            }
        }
        headerBuffer.clear();
        return headerOs.toString();
    }


    /**
     * 准备
     * <p/>
     *
     * @param url
     * @throws IOException
     */
    private void prepare(URL url) throws IOException {
        socketChannel = SocketChannel.open();
        int port = url.getPort();
        if (port == -1)
            port = DEFAULT_PORT;

        //异步socket
        socketChannel.configureBlocking(false);
        selector = Selector.open();
        //超时
        socketChannel.socket().setSoTimeout(timeout);
        //注册
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        InetSocketAddress address = new InetSocketAddress(url.getHost(), port);
        int errNum = 5;
        try {
            socketChannel.connect(address);
        } catch (IOException e) {
            if (errNum-- > 0) {
                socketChannel.connect(address);
            } else {
                throw new IOException(e);
            }
        }
        _init_file();
    }

    public void _init_file() throws IOException {
        File dir = new File(fileBean.getSavePath());
        if (!dir.exists())
            dir.mkdirs();
        if (!dir.canWrite())
            throw new UnsupportedOperationException(fileBean.getSavePath() + " 不支持写文件操作");
        String path = fileBean.getSavePath().concat(File.separator).concat(fileBean.getSaveName());
        path = StringUtils.normalize(path);
        downloadFile = new File(path);
        if (downloadFile.exists()) {
            start = downloadFile.length();
        }
    }

    public FileChannel getOutChannel() throws IOException {
        RandomAccessFile accessFile = new RandomAccessFile(downloadFile, "rw");
        if (rangeSupport) {
            // accessFile.seek(start);
        }
        return accessFile.getChannel();
    }


    public void addListener(Listener listener) {
        super.addObserver(listener);
    }

}
